package com.gzs.learn.payment.util;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.gzs.learn.payment.inf.exception.PaymentException;
import com.gzs.learn.payment.model.cfg.TokenAlipay;
import com.gzs.learn.payment.model.cfg.TokenYinlian;
import net.sf.cglib.beans.BeanMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.gzs.learn.payment.util.AssertUtil.checkBlankStr;
import static com.gzs.learn.payment.util.AssertUtil.checkObjNotNull;


/**
 * 签名工具类
 *
 * @author guanzhisong
 * @date 2017年3月30日
 */
public class SignUtil {
    // 微信签名字段
    private static final String WEIXIN_SIGN = "sign";
    // 微信package字段
    private static final String WEIXIN_PACKAGE = "packAge";
    private static final String WEIXIN_PACKAGE_TARGET = "package";
    // 银联签名字段
    private static final String UNION_PAY_SIGN = "signature";
    // 签名需要转换的键值对
    private static final Map<String, String> CONVERT_MAP = Maps.newHashMap();
    static {
        CONVERT_MAP.put(WEIXIN_PACKAGE, WEIXIN_PACKAGE_TARGET);
    }

    /**
     * 计算微信对象签名
     *
     * @param obj
     * @param key
     * @return
     */
    public static String weixinSign(Object obj, String key) {
        @SuppressWarnings("unchecked")
        Map<String, Object> objMap = BeanMap.create(obj);
        String param = objMap.entrySet().stream().filter(e -> {
            return filterEntry(e.getKey(), e.getValue(), WEIXIN_SIGN);
        }).map(e -> {
            return buildKVParam(e.getKey(), e.getValue(), CONVERT_MAP);
        }).sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.joining("&"));
        param += "&key=" + key;
        return Md5Util.getMd5(param).toUpperCase();
    }

    private static String buildKVParam(String key, Object v, Map<String, String> convertMap) {
        String convertKey = convertMap.get(key);
        if (StringUtils.isNotBlank(convertKey)) {
            return convertKey + "=" + v;
        }
        return key + "=" + v;
    }

    private static boolean filterEntry(String k, Object v, String... ingore) {
        List<String> ingoreKeys = Lists.newArrayList(ingore);
        if (ingoreKeys.contains(k)) {
            return false;
        }
        return v != null && !String.valueOf(v).trim().equals("");
    }

    /**
     * 微信回调签名验证
     *
     * @param notifyXml 待验签字符串
     * @param sign 签名
     * @param key 微信api秘钥
     * @return
     * @throws Exception
     */
    public static boolean weixinSignVerify(String notifyXml, String sign, String key) throws Exception {
        checkObjNotNull(notifyXml, "weixin sign verify obj is null");
        checkBlankStr(sign, "weixin sign verify sign is null");
        checkBlankStr(key, "weixin sign verify key is null");
        Map<String, String> map = CommonsUtil.xmlToMap(notifyXml);
        String sortStr =
                map.entrySet().stream()
                .filter(e -> filterEntry(e.getKey(), e.getValue(), WEIXIN_SIGN))
                .map(e -> {
                    return buildKVParam(e.getKey(), e.getValue(), CONVERT_MAP);
                }).sorted(String.CASE_INSENSITIVE_ORDER)
                .collect(Collectors.joining("&"));
        sortStr += "&key=" + key;
        return Md5Util.getMd5(sortStr).toUpperCase().equalsIgnoreCase(sign);
    }

    /**
     * 对对象做签名
     *
     * @param obj
     * @param tokenyinlian
     * @return
     */
    public static String yinlianSign(Object obj, TokenYinlian tokenyinlian) {
        // 对类的所有属性按照asc码排序
        @SuppressWarnings("unchecked")
        Map<String, Object> objMap = BeanMap.create(obj);
        try {
            String stringData = objMap.entrySet().stream()
                    .filter(e -> filterEntry(e.getKey(), e.getValue(), UNION_PAY_SIGN))
                    .map(e -> buildKVParam(e.getKey(), e.getValue(), Maps.newHashMap()))
                    .sorted(String.CASE_INSENSITIVE_ORDER)
                    .collect(Collectors.joining("&"));
            // 通过SHA1进行摘要并转16进制
            byte[] signDigest = sha1X16(stringData, "UTF-8");
            PrivateKey privateKey = getPrivateKey(tokenyinlian);
            Signature st = Signature.getInstance("SHA1withRSA", "BC");
            st.initSign(privateKey);
            st.update(signDigest);
            byte[] softsign = st.sign();
            return Base64.encodeBase64String(softsign);
        } catch (Exception e) {
            throw new PaymentException(e);
        }
    }

    private static PrivateKey getPrivateKey(TokenYinlian tokenyinlian)
            throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
        String priKey = tokenyinlian.getCertkey();
        PKCS8EncodedKeySpec priPKCS8;
        priPKCS8 = new PKCS8EncodedKeySpec(Base64Utils.decode(priKey));
        KeyFactory keyf = KeyFactory.getInstance("RSA");
        PrivateKey protectedKey = keyf.generatePrivate(priPKCS8);
        return protectedKey;
    }

    private static byte[] sha1X16(String data, String encoding) throws Exception {
        byte[] byteDatas = data.getBytes(encoding);
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.reset();
        md.update(byteDatas);
        byte[] bytes = md.digest();
        StringBuilder sha1StrBuff = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            if (Integer.toHexString(0xFF & bytes[i]).length() == 1) {
                sha1StrBuff.append("0").append(Integer.toHexString(0xFF & bytes[i]));
            } else {
                sha1StrBuff.append(Integer.toHexString(0xFF & bytes[i]));
            }
        }
        return sha1StrBuff.toString().getBytes(encoding);
    }

    /**
     * 银联签名验证算法
     *
     * @param obj
     * @param tokenYinlian
     * @return
     */
    @SuppressWarnings("unchecked")
    public static boolean yinlianSignVerify(Object obj, TokenYinlian tokenYinlian) {
        if (obj == null || tokenYinlian == null) {
            return false;
        }

        Map<String, Object> objMap = BeanMap.create(obj);
        // 对类的所有属性按照asc码排序
        try {
            String sign = String.valueOf(objMap.get(UNION_PAY_SIGN));
            String param = objMap.entrySet().stream()
                    .filter(e -> filterEntry(e.getKey(), e.getValue(), UNION_PAY_SIGN))
                    .map(e -> buildKVParam(e.getKey(), e.getValue(), Maps.newHashMap()))
                    .sorted(String.CASE_INSENSITIVE_ORDER)
                    .collect(Collectors.joining("&"));

            byte[] srcData = sha1X16(param, "UTF-8");
            byte[] destData = Base64Utils.decode(sign);
            PublicKey publicKey = getPublicKey(tokenYinlian.getVerkey());
            Signature st = Signature.getInstance("SHA1withRSA", "BC");
            st.initVerify(publicKey);
            st.update(srcData);
            boolean result = st.verify(destData);
            return result;
        } catch (Exception e) {
            throw new PaymentException(e);
        }
    }

    private static PublicKey getPublicKey(String pKey) throws Exception {
        // 自己的公钥(测试)
        X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(Base64Utils.decode(pKey));
        // RSA对称加密算法
        KeyFactory keyFactory;
        keyFactory = KeyFactory.getInstance("RSA");
        // 取公钥匙对象
        PublicKey publicKey = keyFactory.generatePublic(bobPubKeySpec);
        return publicKey;
    }

    @SuppressWarnings("unchecked")
    public static String alipaySign(Object bean, TokenAlipay token, boolean isReturnALl) {
        Map<String, Object> objMap = BeanMap.create(bean);
        // 对类的所有属性按照asc码排序
        String params = objMap.entrySet().stream()
                .filter(e -> filterEntry(e.getKey(), e.getValue(), "bizContent", "sign"))
                .map(e -> buildKVParam(e.getKey(), e.getValue(), Maps.newHashMap()))
                .sorted(String.CASE_INSENSITIVE_ORDER)
                .collect(Collectors.joining("&"));

        try {
            String signStr = rsaSign(params, token.getPrikey(), "utf-8");
            params += "&sign=" + signStr;
            if (isReturnALl) {
                return doUrlEncode(params);
            } else {
                return signStr;
            }
        } catch (Exception e) {
            throw new PaymentException(e);
        }
    }

    private static String rsaSign(String content, String privateKey, String charset) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey priKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
        Signature signature = Signature.getInstance("SHA1WithRSA");
        signature.initSign(priKey);
        if (StringUtils.isEmpty(charset)) {
            signature.update(content.getBytes());
        } else {
            signature.update(content.getBytes(charset));
        }
        byte[] signed = signature.sign();
        String result = new String(Base64.encodeBase64(signed));
        return result;
    }

    private static String doUrlEncode(String inputStr) throws UnsupportedEncodingException {
        if (StringUtils.isBlank(inputStr)) {
            return "";
        }
        String[] arrStr = inputStr.split("&");
        String result = "";
        for (String str : arrStr) {
            if (StringUtils.isBlank(str)) {
                continue;
            }
            int pos = str.indexOf("=");
            if (pos <= 0) {
                continue;
            }
            String keyItem = str.substring(0, pos);
            String valItem = str.substring(pos + 1);
            if (StringUtils.isNotBlank(result)) {
                result += "&";
            }
            result += keyItem;
            result += "=";
            result += URLEncoder.encode(valItem, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
        }
        return result;
    }
}
